import { PortalContext } from '@/components/PortalProvider';
import { nanoid } from 'nanoid';

import React, {
  Dispatch,
  Fragment,
  SetStateAction,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState
} from 'react';

type OverlayProps<T> = T extends { onClose: infer U } ? Omit<T, 'onClose'> & { onClose?: U } : T;

type OpenOverlayProps<T> = {
  // 用于标识打开实例
  name?: string;

  // Overlay组件：Modal Drawer ...
  Overlay: React.FC<T>;

  // 传给Overlay组件的属性值
  props: OverlayProps<T> & {
    resolve?: (value?: any) => void;
    reject?: (reason?: any) => void;
  };

  // Overlay组件控制打开和关闭的属性名，props存在isOpen open等布尔类型属性可忽略此参数
  // 如果未设置且props没有相应属性，则使用注册组件控制显示Overlay组件，反注册控制关闭Overlay组件
  openProp?: string;

  // 关闭后是否保留Overlay组件状态，openProp为空时忽略此属性
  isKeepAlive?: boolean;

  // 强制Overlay组件注册到Portal中
  isUsePortal?: boolean;

  // 强制Overlay组件注册到Portal中，且当前组件卸载后也不会被关闭
  isStandalone?: boolean;
};

type OverlayManagerType = {
  // 打开Overlay组件，如： Modal Drawer ...
  openOverlay: <T>(props: OpenOverlayProps<T>) => Promise<any>;

  // 关闭Overlay组件，参数为打开时传入的name或Overlay
  closeOverlay: <T>(name: string | React.FC<T>) => void;

  // 关闭所有Overlay组件
  closeAllOverlays: () => void;

  // 返回Overlay组件是否已打开，参数为打开时传入的name或Overlay
  isOverlayOpen: <T>(name: string | React.FC<T>) => boolean;

  // Overlay组件容器，如果未在目标组件中注册此容器，所有Overlay组件都将注册到外部的Portal中
  // 如果在目标组件中注册此容器，且openOverlay参数属性isUsePortal和isStandalone都不为true时，Overlay组件将注册到此容器中
  // 主要用于控制Overlay组件的层级
  OverlayContainer: React.FC<any>;
};

type OverlayType = {
  name?: string;
  Overlay: React.FC<any>;
  props: any;
  openProp?: string;
  isKeepAlive?: boolean;
  isUsePortal?: boolean;
  isStandalone?: boolean;
  key: string;
  isInPortal?: boolean;
  isOpen?: boolean;
  onClose?: (...arg: any) => void;
  resolve?: (value?: any) => void;
  reject?: (reason?: any) => void;
};

const animateDuration = 5000;

export function useOverlayManager(): OverlayManagerType {
  const overlaysRef = useRef<OverlayType[]>([]);

  const setOverlaysRef = useRef<Dispatch<SetStateAction<OverlayType[]>>>(undefined);

  const { addToPortal, removeFromPortal } = useContext(PortalContext);

  const handleClose = useCallback(
    (overlay: OverlayType, ...args: any) => {
      const { Overlay, props, openProp, isKeepAlive, key, isInPortal, onClose, resolve } = overlay;

      overlay.isOpen = false;

      if (!openProp) {
        overlaysRef.current = overlaysRef.current.filter((item) => item.key !== key);
        if (isInPortal) {
          removeFromPortal(key);
        } else {
          setOverlaysRef.current?.((state) => state.filter((it) => it.key !== key));
        }
        onClose?.(...args);
        resolve?.(...args);
        return;
      }

      props[openProp] = false;

      if (isInPortal) {
        addToPortal(key, <Overlay {...props} />);
      } else {
        setOverlaysRef.current?.((state) =>
          state.map((item) => (item.key === key ? { ...overlay } : item))
        );
      }

      if (isKeepAlive) {
        onClose?.(...args);
        resolve?.(...args);
        return;
      }

      setTimeout(() => {
        if (overlay.isOpen) {
          return;
        }

        overlaysRef.current = overlaysRef.current.filter((item) => item.key !== key);
        if (isInPortal) {
          removeFromPortal(key);
        } else {
          setOverlaysRef.current?.((state) => state.filter((it) => it.key !== key));
        }
      }, animateDuration);
    },
    [addToPortal, removeFromPortal]
  );

  const openOverlay = useCallback(
    function <T>({
      Overlay,
      props,
      name,
      openProp,
      isKeepAlive,
      isUsePortal,
      isStandalone
    }: OpenOverlayProps<T>): Promise<any> {
      return new Promise((resolve, reject) => {
        const overlay =
          overlaysRef.current.find((item) =>
            name ? item.name === name : item.isKeepAlive && !item.name && item.Overlay === Overlay
          ) ||
          ({
            Overlay,
            props,
            name,
            openProp:
              openProp ||
              ['isOpen', 'open'].find((key) => typeof (props as any)?.[key] === 'boolean'),
            isKeepAlive,
            isUsePortal,
            isStandalone,
            key: ''
          } as OverlayType);

        const isNew = !overlay.key;

        if (isNew) {
          overlay.key = nanoid();
          overlaysRef.current.push(overlay);
        }

        overlay.props = {
          ...props,
          onClose: (...args: any) => handleClose(overlay, ...args),
          resolve,
          reject
        } as T;

        if (overlay.openProp) {
          (overlay.props as any)[overlay.openProp] = true;
        }

        overlay.isOpen = true;

        if (isUsePortal || isStandalone || !setOverlaysRef.current) {
          !isNew &&
            !overlay.isInPortal &&
            setOverlaysRef.current?.((state) => state.filter((it) => it.key !== overlay.key));

          overlay.isInPortal = true;
          addToPortal(overlay.key, <overlay.Overlay {...overlay.props} />);
        } else {
          !isNew && overlay.isInPortal && removeFromPortal(overlay.key);

          overlay.isInPortal = false;
          setOverlaysRef.current((state) =>
            isNew
              ? [...state, { ...overlay }]
              : state.map((it) => (it.key === overlay.key ? { ...overlay } : it))
          );
        }
      });
    },
    [addToPortal, removeFromPortal, handleClose]
  );

  const closeOverlay = useCallback(
    (name: string | React.FC<any>) => {
      if (!name) {
        return;
      }
      if (typeof name === 'string') {
        const overlay = overlaysRef.current.find((item) => item.name === name);
        overlay?.isOpen && handleClose(overlay);
      } else {
        overlaysRef.current.forEach(
          (it) => it.isOpen && !it.name && it.Overlay === name && handleClose(it)
        );
      }
    },
    [handleClose]
  );

  const closeAllOverlays = useCallback(() => {
    overlaysRef.current.forEach((it) => {
      handleClose(it);
    });
  }, [handleClose]);

  const isOverlayOpen = useCallback(
    (name: string | React.FC<any>) =>
      !!name &&
      overlaysRef.current.some((it) => it.isOpen && (it.name === name || it.Overlay === name)),
    []
  );

  const OverlayContainer = useCallback(() => {
    const [overlays, setOverlays] = useState<OverlayType[]>([]);

    useEffect(() => {
      setOverlaysRef.current = setOverlays;
      return () => {
        setOverlaysRef.current = undefined;
      };
    }, []);

    return (
      <>
        {overlays.map((it) => (
          <Fragment key={it.key}>
            <it.Overlay {...it.props} />
          </Fragment>
        ))}
      </>
    );
  }, []);

  useEffect(
    () => () =>
      overlaysRef.current.forEach(
        (it) => it.isInPortal && !it.isStandalone && removeFromPortal(it.key)
      ),
    [removeFromPortal]
  );

  return {
    openOverlay,
    closeOverlay,
    closeAllOverlays,
    isOverlayOpen,
    OverlayContainer
  };
}
